import random
import math
import os

import numpy as np
import pandas as pd
from operator import itemgetter
from collections import defaultdict

random.seed(0)


class UserBasedCF(object):
    """
    基于usercf的电影推荐
    """

    def __init__(self, path, itemcontentpath):
        self.n_sim_user = 20  # 推荐相似用户的个数
        self.n_rec_item = 10  # 为用户推荐电影数

        self.dataPath = path
        self.data = self.loadData()  # 加载数据集
        self.user_rating_matrix = self.generateUserRatingData(itemcontentpath)
        self.trainData, self.testData = self.splitData(0.7)  # 划分训练集和测试集
        # self.user_sim_mat = self.userSimilarity()
        self.user_sim_mat = self.userCosineSimilarity()

    def loadData(self):
        """
        打开数据文件，加载数据
        :return:
        """
        print("加载数据集...")
        data = []  # 定义数据数组
        # TODO：1.使用open方法加载文件
        fp = open(self.dataPath)
        # TODO：2.使用for循环遍历文件，获取每行信息
        for line in fp:
            userid, itemid, rating, _ = line.split("::")
            data.append((int(userid), int(itemid), int(rating)))
        return data

    def generateUserRatingData(self, itemPath):
        """
        生成用户评分矩阵
        :return:
        """
        print('开始处理数据...')
        itemContentMatrix = pd.read_csv(open(itemPath, encoding="unicode_escape"), engine="python", sep="::",
                                        header=None)

        # TODO：1.获取item个数（多少个电影）
        itemSize = itemContentMatrix[0].max()
        # TODO：2.获取用户个数（多少个用户）
        userSize = len(set([item[0] for item in self.data]))
        # TODO：3.构建用户对电影的评分矩阵
        rating_matrix = np.zeros((userSize + 1, itemSize + 1))
        for rating_item in self.data:
            rating_matrix[rating_item[0], rating_item[1]] = rating_item[2]
        return rating_matrix

    def splitData(self, pivot=0.7):
        """
        划分训练集，测试集
        :param pivot:划分比例
        :return:
        """
        print("划分训练集和测试集...")
        train, test = {}, {}  # 定义字典

        # TODO:1.遍历循环每一行数据
        for userid, itemid, rating in self.data:

            # TODO:2.划分训练集和测试集，比例pivot
            # TODO:3.构建字典内容，主键key:userId,value:item-rating
            if random.random() < pivot:
                train.setdefault(userid, {})
                train[userid][itemid] = rating
            else:
                test.setdefault(userid, {})
                test[userid][itemid] = rating
        return train, test

    def userSimilarity(self):
        """
        计算用户相似度
        :return:
        """
        print("计算用户与用户之间的相似度...")
        # TODO:  1.计算每个item被多少user评价过
        item_users = {}  # 构建字典
        for user, items in self.trainData.items():
            for item in items:
                if item not in item_users:
                    item_users[item] = set()
                item_users[item].add(user)
        # TODO:  2.构建倒查表
        count = {}  # 记录用户u,v共同评价过的电影数目
        user_item_count = {}  # 记录user评价次数
        for item, users in item_users.items():
            for u in users:
                user_item_count.setdefault(u, 0)
                user_item_count[u] += 1
                count.setdefault(u, defaultdict(int))
                for v in users:
                    if u == v:
                        continue
                    count[u][v] += 1
        # TODO: 3.构建相似度矩阵
        userSim = dict()  # 定义相似度矩阵
        for u, related_users in count.items():
            userSim.setdefault(u, {})
            for v, num in related_users.items():
                userSim[u][v] = num / math.sqrt(user_item_count[u] * user_item_count[u])
        return userSim

    def calCosineSimilarity(self, vec1, vec2):
        return np.dot(vec1, vec2.T) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

    def userCosineSimilarity(self):
        """
        计算用户相似度
        :return:
        """
        print("计算用户与用户之间的相似度...")
        # TODO:  1.计算user相似用户
        userSim = defaultdict(dict)
        for i in range(1, len(self.user_rating_matrix) - 1):
            for j in range(i + 1, len(self.user_rating_matrix)):
                distance = self.calCosineSimilarity(self.user_rating_matrix[i], self.user_rating_matrix[j])
                userSim[i][j] = distance
                userSim[j][i] = distance
        return userSim

    def recommend(self, user):
        """
        完成推荐算法
        找到K个与用户user相似用户,并推荐N个电影

        :param user: 用户
        :return:返回最相似的N个电影
        """
        K = self.n_sim_user
        N = self.n_rec_item
        rank = dict()
        # TODO: 1.找到用户评分过的电影
        rating_items = self.trainData.get(user, {})
        # TODO: 2.遍历与user最相似的K个用户
        # TODO: 3.根据推荐算法计算
        # TODO: 4.计算最相似的N个电影，并返回
        for similar_user, similarity_factor in sorted(self.user_sim_mat[user].items(),
                                                      key=itemgetter(1), reverse=True)[0:K]:
            for item, rating in self.trainData[similar_user].items():
                if item in rating_items:
                    continue
                rank.setdefault(item, 0)
                rank[item] += similarity_factor * rating
        return dict(sorted(rank.items(), key=itemgetter(1), reverse=True)[0:N])

    def evaluate(self):
        """
        评价结果
        精确率
        :return:
        """
        print("开始计算...")
        N = self.n_rec_item
        hit = 0
        rec_count = 0
        # TODO:1.遍历训练集中的用户
        # TODO:2.获取测试集中相应的用户看的电影
        # TODO:3.调用recommend推荐算法获取推荐电影
        # TODO:4.计算相应的hit值，精确率
        for user in self.trainData.keys():
            test_items = self.testData.get(user, {})
            rec_items = self.recommend(user)
            for item, _ in rec_items.items():
                if item in test_items:
                    hit += 1
            rec_count += N
        # TODO:5.计算precision
        return hit / (1.0 * rec_count)


if __name__ == '__main__':
    """
    程序入口
    """
    ratingfile = os.path.join('data', 'ratings.dat')
    itemcontentpath = os.path.join('data', 'movies.dat')
    usercf = UserBasedCF(ratingfile, itemcontentpath)
    print("第1个用户的相似用户：", usercf.user_sim_mat[1])
    print(usercf.recommend(1))
    print(usercf.evaluate())
